/*
 * Copyright 2005-2015 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.ldap.config;

import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.parsing.BeanComponentDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.xml.AbstractBeanDefinitionParser;
import org.springframework.beans.factory.xml.BeanDefinitionParser;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.ldap.CommunicationException;
import org.springframework.ldap.core.support.LdapContextSource;
import org.springframework.ldap.pool.PoolExhaustedAction;
import org.springframework.ldap.pool.factory.PoolingContextSource;
import org.springframework.ldap.pool.validation.DefaultDirContextValidator;
import org.springframework.ldap.pool2.factory.PoolConfig;
import org.springframework.ldap.pool2.factory.PooledContextSource;
import org.springframework.ldap.transaction.compensating.manager.TransactionAwareContextSourceProxy;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
import org.springframework.util.xml.DomUtils;
import org.w3c.dom.Element;

import java.util.HashSet;
import java.util.Set;

import static org.springframework.ldap.config.ParserUtils.getBoolean;
import static org.springframework.ldap.config.ParserUtils.getInt;
import static org.springframework.ldap.config.ParserUtils.getString;

/**
 * @author Mattias Hellborg Arthursson
 * @author Eddu Melendez
 */
public class ContextSourceParser implements BeanDefinitionParser {
    private static final String ATT_ANONYMOUS_READ_ONLY = "anonymous-read-only";
    private static final String ATT_AUTHENTICATION_SOURCE_REF = "authentication-source-ref";
    private static final String ATT_AUTHENTICATION_STRATEGY_REF = "authentication-strategy-ref";
    private static final String ATT_BASE = "base";
    private static final String ATT_PASSWORD = "password";
    private static final String ATT_NATIVE_POOLING = "native-pooling";
    private static final String ATT_REFERRAL = "referral";
    private static final String ATT_URL = "url";
    private static final String ATT_BASE_ENV_PROPS_REF = "base-env-props-ref";

    // pooling attributes
    private static final String ATT_MAX_ACTIVE = "max-active";
    private static final String ATT_MAX_TOTAL = "max-total";
    private static final String ATT_MAX_IDLE = "max-idle";
    private static final String ATT_MIN_IDLE = "min-idle";
    private static final String ATT_MAX_WAIT = "max-wait";
    private static final String ATT_WHEN_EXHAUSTED = "when-exhausted";
    private static final String ATT_TEST_ON_BORROW = "test-on-borrow";
    private static final String ATT_TEST_ON_RETURN = "test-on-return";
    private static final String ATT_TEST_WHILE_IDLE = "test-while-idle";
    private static final String ATT_EVICTION_RUN_MILLIS = "eviction-run-interval-millis";
    private static final String ATT_TESTS_PER_EVICTION_RUN = "tests-per-eviction-run";
    private static final String ATT_EVICTABLE_TIME_MILLIS = "min-evictable-time-millis";
    private static final String ATT_VALIDATION_QUERY_BASE = "validation-query-base";
    private static final String ATT_VALIDATION_QUERY_FILTER = "validation-query-filter";
    private static final String ATT_VALIDATION_QUERY_SEARCH_CONTROLS_REF = "validation-query-search-controls-ref";
    private static final String ATT_NON_TRANSIENT_EXCEPTIONS = "non-transient-exceptions";
    private static final String ATT_MAX_IDLE_PER_KEY = "max-idle-per-key";
    private static final String ATT_MIN_IDLE_PER_KEY = "min-idle-per-key";
    private static final String ATT_MAX_TOTAL_PER_KEY = "max-total-per-key";
    private static final String ATT_EVICTION_POLICY_CLASS = "eviction-policy-class";
    private static final String ATT_FAIRNESS = "fairness";
    private static final String ATT_JMX_ENABLE = "jmx-enable";
    private static final String ATT_JMX_NAME_BASE = "jmx-name-base";
    private static final String ATT_JMX_NAME_PREFIX = "jmx-name-prefix";
    private static final String ATT_LIFO = "lifo";
    private static final String ATT_BLOCK_WHEN_EXHAUSTED = "block-when-exhausted";
    private static final String ATT_TEST_ON_CREATE = "test-on-create";
    private static final String ATT_SOFT_MIN_EVICTABLE_IDLE_TIME_MILLIS = "soft-min-evictable-idle-time-millis";


    private static final String ATT_USERNAME = "username";
    static final String DEFAULT_ID = "contextSource";
    private static final int DEFAULT_MAX_ACTIVE = 8;
    private static final int DEFAULT_MAX_TOTAL = -1;
    private static final int DEFAULT_MAX_IDLE = 8;
    private static final int DEFAULT_MIN_IDLE = 0;
    private static final int DEFAULT_MAX_WAIT = -1;
    private static final int DEFAULT_EVICTION_RUN_MILLIS = -1;
    private static final int DEFAULT_TESTS_PER_EVICTION_RUN = 3;
    private static final int DEFAULT_EVICTABLE_MILLIS = 1000 * 60 * 30;
    private static final int DEFAULT_MAX_TOTAL_PER_KEY = 8;
    private static final int DEFAULT_MAX_IDLE_PER_KEY = 8;
    private static final int DEFAULT_MIN_IDLE_PER_KEY = 0;
    private static final String DEFAULT_EVICTION_POLICY_CLASS_NAME =
            "org.apache.commons.pool2.impl.DefaultEvictionPolicy";
    private static final boolean DEFAULT_FAIRNESS = false;
    private static final boolean DEFAULT_JMX_ENABLE = true;
    private static final String DEFAULT_JMX_NAME_BASE = null;
    private static final String DEFAULT_JMX_NAME_PREFIX = "ldap-pool";
    private static final boolean DEFAULT_LIFO = true;
    private static final int DEFAULT_MAX_WAIT_MILLIS = -1;
    private static final boolean DEFAULT_BLOCK_WHEN_EXHAUSTED = true;
    private static final int DEFAULT_SOFT_MIN_EVICTABLE_IDLE_TIME_MILLIS = -1;

    @Override
    public BeanDefinition parse(Element element, ParserContext parserContext) {
        BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(LdapContextSource.class);

        String username = element.getAttribute(ATT_USERNAME);
        String password = element.getAttribute(ATT_PASSWORD);
        String url = element.getAttribute(ATT_URL);
        Assert.hasText(url, "url attribute must be specified");

        builder.addPropertyValue("userDn", username);
        builder.addPropertyValue("password", password);

        BeanDefinitionBuilder urlsBuilder = BeanDefinitionBuilder
                .rootBeanDefinition(UrlsFactory.class)
                .setFactoryMethod("urls")
                .addConstructorArgValue(url);

        builder.addPropertyValue("urls", urlsBuilder.getBeanDefinition());
        builder.addPropertyValue("base", getString(element, ATT_BASE, ""));
        builder.addPropertyValue("referral", getString(element, ATT_REFERRAL, null));

        boolean anonymousReadOnly = getBoolean(element, ATT_ANONYMOUS_READ_ONLY, false);
        builder.addPropertyValue("anonymousReadOnly", anonymousReadOnly);
        boolean nativePooling = getBoolean(element, ATT_NATIVE_POOLING, false);
        builder.addPropertyValue("pooled", nativePooling);

        String authStrategyRef = element.getAttribute(ATT_AUTHENTICATION_STRATEGY_REF);
        if(StringUtils.hasText(authStrategyRef)) {
            builder.addPropertyReference("authenticationStrategy", authStrategyRef);
        }

        String authSourceRef = element.getAttribute(ATT_AUTHENTICATION_SOURCE_REF);
        if(StringUtils.hasText(authSourceRef)) {
            builder.addPropertyReference("authenticationSource", authSourceRef);
        } else {
            Assert.hasText(username, "username attribute must be specified unless an authentication-source-ref explicitly configured");
            Assert.hasText(password, "password attribute must be specified unless an authentication-source-ref explicitly configured");
        }

        String baseEnvPropsRef = element.getAttribute(ATT_BASE_ENV_PROPS_REF);
        if(StringUtils.hasText(baseEnvPropsRef)) {
            builder.addPropertyReference("baseEnvironmentProperties", baseEnvPropsRef);
        }

        BeanDefinition targetContextSourceDefinition = builder.getBeanDefinition();
        targetContextSourceDefinition = applyPoolingIfApplicable(targetContextSourceDefinition, element, nativePooling);

        BeanDefinition actualContextSourceDefinition = targetContextSourceDefinition;
        if (!anonymousReadOnly) {
            BeanDefinitionBuilder proxyBuilder = BeanDefinitionBuilder.rootBeanDefinition(TransactionAwareContextSourceProxy.class);
            proxyBuilder.addConstructorArgValue(targetContextSourceDefinition);
            actualContextSourceDefinition = proxyBuilder.getBeanDefinition();
        }

        String id = getString(element, AbstractBeanDefinitionParser.ID_ATTRIBUTE, DEFAULT_ID);
        parserContext.registerBeanComponent(new BeanComponentDefinition(actualContextSourceDefinition, id));

        return actualContextSourceDefinition;
    }

    private BeanDefinition applyPoolingIfApplicable(
            BeanDefinition targetContextSourceDefinition,
            Element element,
            boolean nativePooling) {

        Element poolingElement = DomUtils.getChildElementByTagName(element, Elements.POOLING);
        Element pooling2Element = DomUtils.getChildElementByTagName(element, Elements.POOLING2);

        if (pooling2Element != null && poolingElement != null) {
            throw new IllegalArgumentException(
                    String.format("%s cannot be enabled together with %s.", Elements.POOLING2, Elements.POOLING));
        } else if (poolingElement == null && pooling2Element == null) {
            return targetContextSourceDefinition;
        }

        if(nativePooling) {
            throw new IllegalArgumentException(
                    String.format("%s cannot be enabled together with %s", ATT_NATIVE_POOLING, Elements.POOLING));
        }

        if (pooling2Element != null) {
            BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(PooledContextSource.class);
            builder.addPropertyValue("contextSource", targetContextSourceDefinition);

            populatePoolConfigProperties(builder, pooling2Element);

            boolean testOnBorrow = getBoolean(pooling2Element, ATT_TEST_ON_BORROW, false);
            boolean testOnReturn = getBoolean(pooling2Element, ATT_TEST_ON_RETURN, false);
            boolean testWhileIdle = getBoolean(pooling2Element, ATT_TEST_WHILE_IDLE, false);
            boolean testOnCreate = getBoolean(pooling2Element, ATT_TEST_ON_CREATE, false);

            if (testOnBorrow || testOnCreate || testWhileIdle || testOnReturn) {
                populatePoolValidationProperties(builder, pooling2Element);
            }

            return builder.getBeanDefinition();
        } else {
            BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(PoolingContextSource.class);
            builder.addPropertyValue("contextSource", targetContextSourceDefinition);

            builder.addPropertyValue("maxActive", getString(poolingElement, ATT_MAX_ACTIVE, String.valueOf(DEFAULT_MAX_ACTIVE)));
            builder.addPropertyValue("maxTotal", getString(poolingElement, ATT_MAX_TOTAL, String.valueOf(DEFAULT_MAX_TOTAL)));
            builder.addPropertyValue("maxIdle", getString(poolingElement, ATT_MAX_IDLE, String.valueOf(DEFAULT_MAX_IDLE)));
            builder.addPropertyValue("minIdle", getString(poolingElement, ATT_MIN_IDLE, String.valueOf(DEFAULT_MIN_IDLE)));
            builder.addPropertyValue("maxWait", getString(poolingElement, ATT_MAX_WAIT, String.valueOf(DEFAULT_MAX_WAIT)));
            String whenExhausted = getString(poolingElement, ATT_WHEN_EXHAUSTED, PoolExhaustedAction.BLOCK.name());
            builder.addPropertyValue("whenExhaustedAction", PoolExhaustedAction.valueOf(whenExhausted).getValue());
            builder.addPropertyValue("timeBetweenEvictionRunsMillis", getString(poolingElement, ATT_EVICTION_RUN_MILLIS, String.valueOf(DEFAULT_EVICTION_RUN_MILLIS)));
            builder.addPropertyValue("minEvictableIdleTimeMillis", getString(poolingElement, ATT_EVICTABLE_TIME_MILLIS, String.valueOf(DEFAULT_EVICTABLE_MILLIS)));
            builder.addPropertyValue("numTestsPerEvictionRun", getString(poolingElement, ATT_TESTS_PER_EVICTION_RUN, String.valueOf(DEFAULT_TESTS_PER_EVICTION_RUN)));

            boolean testOnBorrow = getBoolean(poolingElement, ATT_TEST_ON_BORROW, false);
            boolean testOnReturn = getBoolean(poolingElement, ATT_TEST_ON_RETURN, false);
            boolean testWhileIdle = getBoolean(poolingElement, ATT_TEST_WHILE_IDLE, false);

            if (testOnBorrow || testOnReturn || testWhileIdle) {
                populatePoolValidationProperties(builder, poolingElement, testOnBorrow, testOnReturn, testWhileIdle);
            }

            return builder.getBeanDefinition();
        }
    }

    private void populatePoolValidationProperties(BeanDefinitionBuilder builder, Element element,
                                                  boolean testOnBorrow, boolean testOnReturn, boolean testWhileIdle) {

        builder.addPropertyValue("testOnBorrow", testOnBorrow);
        builder.addPropertyValue("testOnReturn", testOnReturn);
        builder.addPropertyValue("testWhileIdle", testWhileIdle);

        BeanDefinitionBuilder validatorBuilder = BeanDefinitionBuilder.rootBeanDefinition(DefaultDirContextValidator.class);
        validatorBuilder.addPropertyValue("base", getString(element, ATT_VALIDATION_QUERY_BASE, ""));
        validatorBuilder.addPropertyValue("filter",
                getString(element, ATT_VALIDATION_QUERY_FILTER, DefaultDirContextValidator.DEFAULT_FILTER));
        String searchControlsRef = element.getAttribute(ATT_VALIDATION_QUERY_SEARCH_CONTROLS_REF);
        if(StringUtils.hasText(searchControlsRef)) {
            validatorBuilder.addPropertyReference("searchControls", searchControlsRef);
        }
        builder.addPropertyValue("dirContextValidator", validatorBuilder.getBeanDefinition());

        builder.addPropertyValue("timeBetweenEvictionRunsMillis", getString(element, ATT_EVICTION_RUN_MILLIS, String.valueOf(DEFAULT_EVICTION_RUN_MILLIS)));
        builder.addPropertyValue("numTestsPerEvictionRun", getInt(element, ATT_TESTS_PER_EVICTION_RUN, DEFAULT_TESTS_PER_EVICTION_RUN));
        builder.addPropertyValue("minEvictableIdleTimeMillis", getString(element, ATT_EVICTABLE_TIME_MILLIS, String.valueOf(DEFAULT_EVICTABLE_MILLIS)));

        String nonTransientExceptions = getString(element, ATT_NON_TRANSIENT_EXCEPTIONS, CommunicationException.class.getName());
        String[] strings = StringUtils.commaDelimitedListToStringArray(nonTransientExceptions);
        Set<Class<?>> nonTransientExceptionClasses = new HashSet<Class<?>>();
        for (String className : strings) {
            try {
                nonTransientExceptionClasses.add(ClassUtils.getDefaultClassLoader().loadClass(className));
            } catch (ClassNotFoundException e) {
                throw new IllegalArgumentException(String.format("%s is not a valid class name", className), e);
            }
        }

        builder.addPropertyValue("nonTransientExceptions", nonTransientExceptionClasses);
    }


    private void populatePoolValidationProperties(BeanDefinitionBuilder builder, Element element) {

        BeanDefinitionBuilder validatorBuilder = BeanDefinitionBuilder.rootBeanDefinition(
                org.springframework.ldap.pool2.validation.DefaultDirContextValidator.class);
        validatorBuilder.addPropertyValue("base", getString(element, ATT_VALIDATION_QUERY_BASE, ""));
        validatorBuilder.addPropertyValue("filter",
                getString(element, ATT_VALIDATION_QUERY_FILTER,
                        org.springframework.ldap.pool2.validation.DefaultDirContextValidator.DEFAULT_FILTER));
        String searchControlsRef = element.getAttribute(ATT_VALIDATION_QUERY_SEARCH_CONTROLS_REF);
        if(StringUtils.hasText(searchControlsRef)) {
            validatorBuilder.addPropertyReference("searchControls", searchControlsRef);
        }
        builder.addPropertyValue("dirContextValidator", validatorBuilder.getBeanDefinition());

        String nonTransientExceptions = getString(element, ATT_NON_TRANSIENT_EXCEPTIONS, CommunicationException.class.getName());
        String[] strings = StringUtils.commaDelimitedListToStringArray(nonTransientExceptions);
        Set<Class<?>> nonTransientExceptionClasses = new HashSet<Class<?>>();
        for (String className : strings) {
            try {
                nonTransientExceptionClasses.add(ClassUtils.getDefaultClassLoader().loadClass(className));
            } catch (ClassNotFoundException e) {
                throw new IllegalArgumentException(String.format("%s is not a valid class name", className), e);
            }
        }

        builder.addPropertyValue("nonTransientExceptions", nonTransientExceptionClasses);
    }

    private void populatePoolConfigProperties(BeanDefinitionBuilder builder, Element element) {
        BeanDefinitionBuilder configBuilder = BeanDefinitionBuilder
                .rootBeanDefinition(PoolConfig.class);

        configBuilder.addPropertyValue("maxTotal", getString(element, ATT_MAX_TOTAL, String.valueOf(DEFAULT_MAX_TOTAL)));
        configBuilder.addPropertyValue("maxTotalPerKey", getString(element, ATT_MAX_TOTAL_PER_KEY, String.valueOf(DEFAULT_MAX_TOTAL_PER_KEY)));
        configBuilder.addPropertyValue("maxIdlePerKey", getString(element, ATT_MAX_IDLE_PER_KEY, String.valueOf(DEFAULT_MAX_IDLE_PER_KEY)));
        configBuilder.addPropertyValue("minIdlePerKey", getString(element, ATT_MIN_IDLE_PER_KEY, String.valueOf(DEFAULT_MIN_IDLE_PER_KEY)));
        configBuilder.addPropertyValue("evictionPolicyClassName", getString(element, ATT_EVICTION_POLICY_CLASS, DEFAULT_EVICTION_POLICY_CLASS_NAME));
        configBuilder.addPropertyValue("fairness", getBoolean(element, ATT_FAIRNESS, DEFAULT_FAIRNESS));
        configBuilder.addPropertyValue("jmxEnabled", getBoolean(element, ATT_JMX_ENABLE, DEFAULT_JMX_ENABLE));
        configBuilder.addPropertyValue("jmxNameBase", getString(element, ATT_JMX_NAME_BASE, DEFAULT_JMX_NAME_BASE));
        configBuilder.addPropertyValue("jmxNamePrefix", getString(element, ATT_JMX_NAME_PREFIX, DEFAULT_JMX_NAME_PREFIX));
        configBuilder.addPropertyValue("lifo", getBoolean(element, ATT_LIFO, DEFAULT_LIFO));
        configBuilder.addPropertyValue("maxWaitMillis", getString(element, ATT_MAX_WAIT, String.valueOf(DEFAULT_MAX_WAIT_MILLIS)));
        configBuilder.addPropertyValue("blockWhenExhausted", Boolean.valueOf(getString(element, ATT_BLOCK_WHEN_EXHAUSTED, String.valueOf(DEFAULT_BLOCK_WHEN_EXHAUSTED))));
        configBuilder.addPropertyValue("testOnBorrow", getBoolean(element, ATT_TEST_ON_BORROW, false));
        configBuilder.addPropertyValue("testOnCreate", getBoolean(element, ATT_TEST_ON_CREATE, false));
        configBuilder.addPropertyValue("testOnReturn", getBoolean(element, ATT_TEST_ON_RETURN, false));
        configBuilder.addPropertyValue("testWhileIdle", getBoolean(element, ATT_TEST_WHILE_IDLE, false));
        configBuilder.addPropertyValue("timeBetweenEvictionRunsMillis", getString(element, ATT_EVICTION_RUN_MILLIS, String.valueOf(DEFAULT_EVICTION_RUN_MILLIS)));
        configBuilder.addPropertyValue("numTestsPerEvictionRun", getString(element, ATT_TESTS_PER_EVICTION_RUN, String.valueOf(DEFAULT_TESTS_PER_EVICTION_RUN)));
        configBuilder.addPropertyValue("minEvictableIdleTimeMillis", getString(element, ATT_EVICTABLE_TIME_MILLIS, String.valueOf(DEFAULT_EVICTABLE_MILLIS)));
        configBuilder.addPropertyValue("softMinEvictableIdleTimeMillis", getString(element, ATT_SOFT_MIN_EVICTABLE_IDLE_TIME_MILLIS, String.valueOf(DEFAULT_SOFT_MIN_EVICTABLE_IDLE_TIME_MILLIS)));

        builder.addConstructorArgValue(configBuilder.getBeanDefinition());
    }

    static class UrlsFactory {
        public static String[] urls(String value) {
            return StringUtils.commaDelimitedListToStringArray(value);
        }
    }
}
